﻿HmJS.$Import('core.element');

/*
---
name: DataBinding
version: 0.2-5
description: A class implementing a one-way mapping from field elements to a javascript object and allowing to use more flexible change events.
             The idea is based on the datalinking plugin for jQuery.
license: MIT-style
download: http://mootools.net/forge/p/databinding
source: http://github.com/DEEO/DataBinding

authors:
- Kevin Chapelier (http://www.deeo.fr/)

requires:
core/1.3:
- Function
- Array
- Class
- Class.Extras
- Element
- Element.Event
- Slick.Finder
provides: [DataBinding]
...
*/

/*
### Implements:

Events, Options

DataBinding Method: constructor {#DataBinding:constructor}
----------------------------------------------------------

### Syntax:

	var db = new DataBinding(element, object, options);

### Arguments:

1. element - (element) Container of the elements to be bound, usually a form or a fieldset element.
2. object - (object) Some hash or class instance.
3. options - (object) See below.

### Options:

* mapping - (object) A hash mapping the object properties to custom selectors (otherwise "[name=NAMEOFPROPERTY]" is used).
* assign - (boolean: defaults to true) Whether to assign the starting value of the object to the elements.

### Events:

* onChange - Called each time the value of the object is modified.
* onChangeNAMEOFPROPERTY - Called each time the value of the property is modified.

### Returns:

* (object) The DataBinding instance.

### Example:

	var o = {
		test: 'data'
	};
	
	var db = new DataBinding('form', o, {
		assign: true,
		mapping: {
			onChange: function(e, object, property, oldPropertyValue, fieldElement) {},
			onChangetest: function(e, object, property, oldPropertyValue, fieldElement) {}
		}
	});


DataBinding Method: cleanEvents {#DataBinding:cleanEvents}
----------------------------------------------------------

Remove all events that were added to elements by the DataBinding instance.


DataBinding Method: updateMapping {#DataBinding:updateMapping}
--------------------------------------------------------------

Update the mapping (custom selectors) of the data binding.

### Syntax:

	m.updateMapping(mapping);

### Arguments:

1. mapping - (object) A hash mapping the object properties to custom selectors (otherwise "[name=NAMEOFPROPERTY]" is used).

### Example:

	var o = {
		test: 'data'
	};
	
	var db = new DataBinding('form', o);
	db.updateMapping({
		test: 'some-element-id'
	});


DataBinding Static Method: assign {#DataBinding:assign}
-------------------------------------------------------

Assign the values of an objects to any element.

### Syntax:

	DataBinding.assign(element, object, options);

### Arguments:

1. element - (element) Container of the elements, usually a form or a fieldset element.
2. object - (object) Some hash or class instance.
3. options - (object) Same as for the DataBinding constructor, only the mapping option will be used.

### Example:

	var o = {
		test: 'data'
	};
	
	DataBinding.assign('form', o, {
		mapping:{
			test: 'some-element-id'
		}
	});


Native: Element {#Element}
==========================

 * Custom Native to allow all of its methods to be used with any DOM element via the dollar function $.
 * These methods are also available on window and document.


Element Method: bindObject {#Element:bindObject}
------------------------------------

Shortcut for the databinding constructor, ensure an object isn't bound multiple times to the same element. 

### Syntax:

	element.bindObject(object, options);

### Arguments:

1. object - (object) Some hash or class instance.
2. options - (object) See Databinding constructor.

### Example:

	var o = {
		test: 'data'
	};
	
	$('form').bindObject(o, {
		mapping:{
			test: 'some-element-id'
		}
	});


Element Method: unbindObject {#Element:unbindObject}
----------------------------------------

Search for a databinding between the element and the object and remove it.

### Syntax:

	element.unbindObject(object);

### Arguments:

1. object - (object) Some hash or class instance.

### Example:

	var o = {
		test: 'data'
	};
	
	$('form').bindObject(o, {
		mapping:{
			test: 'some-element-id'
		}
	});
	
	$('form').unbindObject(o);


Element Method: assignObject {#Element:assignObject}
----------------------------------------

Shortcut for DataBinding:assign.

### Syntax:

	element.assignObject(object, options);

### Arguments:

1. object - (object) Some hash or class instance.
2. options - (object) See Databinding:assign arguments.

### Example:

	var o = {
		test: 'data'
	};
	
	$('form').assignObject(o, {
		mapping:{
			test: 'some-element-id'
		}
	});
*/

HmJS.register('util.dom.DataBinding', function ($ns) {

	var DataBinding = new Class({

		Implements: [Options, Events],

		options: {
			mapping: {},
			assign: true
		},

		events: null,

		elements: null,

		element: null,

		obj: null,

		initialize: function (element, obj, options) {
			this.setOptions(options);
			this.element = $(element);
			this.obj = obj;

			if (this.options.assign) {
				DataBinding.assign(this.element, this.obj, this.options);
			}

			this.updateBindings();
		},

		cleanEvents: function () {
			for (var key in this.events) {
				for (var x = 0, l = this.events[key].length; x < l; x++) {
					this.elements[key][x].removeEvent('change', this.events[key][x]);
				}
			}
		},

		updateMapping: function (mapping) {
			mapping = mapping || {};

			/* first we reset the events */
			this.cleanEvents();

			/* then we update the bindings */
			this.options.mapping = mapping;
			this.updateBindings();

			return this;
		},

		updateBindings: function () {
			if (typeOf(this.obj) != 'object' || typeOf(this.element) != 'element') {
				return;
			}

			this.events = {};
			this.elements = {};

			for (var key in this.obj) {
				var type = typeOf(this.obj[key]);

				if (type == 'null' || type == 'string' || type == 'number' || type == 'array') {
					var mapping = this.options.mapping[key] || '[name=' + key + ']';
					var elements = this.element.getElements(mapping);

					if (elements && elements.length > 0) {
						this.events[key] = [];
						this.elements[key] = elements;
						var self = this;

						//thanks to keeto from the #mootools channel for the syntactic sugar
						var self = this;
						for (var x = elements.length; x--; ) (function (element, key) {
							var fn = function (e) {
								self.updateValue(e, element, key);
							};

							fn.el = element;
							element.addEvent('change', fn);
							self.events[key].push(fn);
						})(elements[x], key);

						if (!this.options.assign) {
							this.obj[key] = this.getPropertyValue(key);
						}
					}
				}
			}

			return this;
		},

		updateValue: function (e, el, key) {
			var oldValue = this.obj[key];

			this.obj[key] = this.getPropertyValue(key);
			this.fireEvent('change' + key, [e, this.obj, key, oldValue, el]);
			this.fireEvent('change', [e, this.obj, key, oldValue, el]);
		},

		getPropertyValue: function (key) {
			var elements = this.elements[key];
			var value = null;

			for (var x = 0, l = elements.length; x < l; x++) {
				var el = elements[x];

				switch (el.tagName.toLowerCase()) {
					case 'input':
						if (el.type == 'checkbox') {
							if (typeOf(value) != 'array') { value = []; }

							if (el.checked) {
								value.push(el.value);
							}

							break;
						}
						else if (el.type == 'radio' && el.checked == false) {
							break;
						}

					default:
						value = el.get('value');
				}
			}

			return value;
		}

	});

	DataBinding.assign = function (el, obj, options) {
		if (typeOf(obj) != 'object' || typeOf(el) != 'element') {
			return;
		}

		var optionsMapping = options ? (options.mapping || {}) : {};

		for (var key in obj) {
			var value = obj[key];
			var type = typeOf(value);

			if (type == 'object') {
				if (value['toString'] && typeOf(value['toString']) == 'function') {
					type = 'string';
					value = value.toString();
				}
			}

			if (type == 'null' || type == 'string' || type == 'number' || type == 'array') {
				var mapping = optionsMapping[key] || '[name=' + key + ']';
				var elements = el.getElements(mapping);

				if (elements && elements.length > 0) {
					for (var x = 0, l = elements.length; x < l; x++) {
						switch (elements[x].tagName.toLowerCase()) {
							case 'input':
								if (elements[x].type == 'checkbox') {
									var checkVal = elements[x].value;
									if (type != 'array') {
										type = 'array';
										value = [value];
										obj[key] = value;
									}

									if ((value.indexOf(checkVal) != -1)) {
										elements[x].checked = true;
									}

									break;
								} else if (elements[x].type == 'radio') {
									if (elements[x].value == value) {
										elements[x].checked = true;
									}
									else {
										elements[x].checked = false;
									}

									break;
								}

							case 'select':
							case 'textarea':
								elements[x].set('value', obj[key]);
								break;

							default:
								elements[x].set('text', obj[key]);
						}
					}
				}
			}
		}
	};

	Element.implement({

		assignObject: function (obj, options) {
			DataBinding.assign(this, obj, options);
			return this;
		},

		bindObject: function (obj, options) {
			var bindings = this.retrieve('databinding') || [];

			for (var x = 0, l = bindings.length; x < l; x++) {
				if (bindings[x].obj === obj) {
					bindings[x].cleanEvents();
					bindings[x] = new DataBinding(this, obj, options);
					this.store('databinding', bindings);
					return this;
				}
			}

			bindings.push(new DataBinding(this, obj, options));
			this.store('databinding', bindings);
			return this;
		},

		unbindObject: function (obj) {
			var bindings = this.retrieve('databinding') || [];

			for (var x = 0, l = bindings.length; x < l; x++) {
				if (bindings[x].obj === obj) {
					bindings[x].cleanEvents();
					bindings.erase(bindings[x]);
				}
			}

			this.store('databinding', bindings);
			return this;
		}

	});

	return DataBinding;
});